#  daemon_dispatcher.rb
#
#  This file is part of myvnet.
#
#  Copyright (C) 2008  Filipe David Borba Manana,  fdmanana@gmail.com
#
#  myvnet is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  myvnet is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with myvnet.  If not, see <http://www.gnu.org/licenses/>.


require 'singleton'
require 'thread'
require 'yaml'
require 'lib/daemon_registry'
require 'lib/packet/injector'
require 'lib/protocols/ethernet'
require 'lib/protocols/icmp'


module Daemon
  
  
  # Dispatches packets captured from the network to all registered daemons.
  #
  class Dispatcher
    include Singleton        
    
    def initialize
      @conf = Config::Settings.instance
      @dreg = Daemon::Registry.instance      
      @packet_queue = []
      @mutex = Mutex.new
      @empty_queue = ConditionVariable.new
      @worker_threads = ThreadGroup.new
      @packet_injector = Packet::Injector.new(@conf.interface)
      
      # Create worker threads.
      # They fetch packets from the queue and deliver
      # them to all registered daemons.
      #
      1.upto(3) do |i|
        
        t = Thread.new(i) { |id|
          Thread.current[:id] = id
          
          while true do
            packet = get_packet
            
            processed = false
            @dreg.each_daemon do |d|
              used = d.process(packet)
              
              if used
                msg = "Daemon `#{d.name}' processed packet:\n"
                msg << (packet.to_yaml)
                $log[:packet].info msg
              end
              
              processed = processed || used
            end
            
            if not processed
              
              if packet.has_udp?
                send_port_unreachable packet
              else
                msg = "Packet not processed by any daemon:\n"
                msg << packet.to_yaml
                $log[:packet].warn msg
              end
              
            end
            
          end
        }
        
        @worker_threads.add t
      end
      
    end
    
    
    # === Synopsis
    #
    #   Daemon::Dispatcher#add_packet(packet)
    #
    # === Args
    # 
    # +packet+::
    #   A packet captured from the network, belonging to the data link layer.
    #   At the moment only Ethernet packets (instances of Ethernet::Packet),
    #   since it is the only supported data link layer protocol.
    # 
    # === Return
    # 
    # No value.
    # 
    # === Description
    # 
    # Adds a packet, captured from the network, to the dispatcher's packet 
    # queue. The packet will then be delivered to all registered daemons.
    #
    def add_packet(packet)
      
      packet.build_upper_layers!

      @mutex.synchronize do
        @packet_queue.push packet                                        
        @empty_queue.signal
      end
      
      if $log[:packet].debug?
        msg = "Packet added to incoming queue:\n"
        msg << packet.to_yaml      
        $log[:packet].debug msg
      end
      
    end
    
    
    private
    
    # === Synopsis
    #
    #   Daemon::Dispatcher#get_packet
    #
    # === Return
    #
    # A packet captured from the network. 
    #
    # === Description
    #
    # A packet that was captured from the network. This packet belongs to
    # the data link layer. At the moment only Ethernet is supported.
    #
    def get_packet
      packet = nil
      
      @mutex.synchronize do
        @empty_queue.wait(@mutex) while @packet_queue.empty?
        packet = @packet_queue.shift        
      end
      
      if $log[:packet].debug?
        thread_id = Thread.current[:id]
        msg = "Packet taken from incoming queue by thread `#{thread_id}':\n"
        msg << (packet.to_yaml)
        $log[:packet].debug msg
      end      
            
      packet
    end
    

    # === Synopsis
    #
    #   Daemon::Dispatcher#send_port_unreachable(packet)
    #
    # === Args
    #
    # The original Ethernet packet.
    # 
    # === Return
    # 
    # Nothing.
    #
    # === Description
    #
    # Invoked when a received Ethernet packet, containing a UDP packet,
    # was not processed by any of the registered daemons. It injects into
    # the network an ICMP destination-unreachable packet, with code
    # port-unreachable, encapsulating the original IPv4 and UDP packets.
    #    
    def send_port_unreachable(packet)
      icmp_packet = ICMP::Packet.new
      icmp_packet.type = :dst_unreachable
      icmp_packet.code = 0x03  # port unreachable
      icmp_packet.checksum = 0
      icmp_packet.payload = packet.payload.dup  # original IP packet
            
      eth_packet = packet.dup
      eth_packet.src, eth_packet.dst = eth_packet.dst, eth_packet.src
      
      ip_packet = eth_packet.payload
      ip_packet.src, ip_packet.dst = ip_packet.dst, ip_packet.src
      ip_packet.id = rand(0xee_1c) + 0x0f_4a      
      ip_packet.payload = icmp_packet
      ip_packet.proto = :icmp
      ip_packet.checksum = 0
      
      msg = "UDP packet not processed by any daemon."
      msg << " Sending an ICMP destination-unreachable"
      msg << " (code port-unreachable) packet.\n"
      $log[:packet].warn msg
      
      @packet_injector.add_packet(eth_packet, "DaemonDispatcher")
    end
    
  end

end